// Copyright (c) 2006 - 2008, Markus Strauch.
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// 
// * Redistributions of source code must retain the above copyright notice, 
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice, 
// this list of conditions and the following disclaimer in the documentation 
// and/or other materials provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
// THE POSSIBILITY OF SUCH DAMAGE.

package net.sf.sdedit.taglet;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import net.sf.sdedit.config.Configuration;
import net.sf.sdedit.config.ConfigurationManager;
import net.sf.sdedit.diagram.Diagram;
import net.sf.sdedit.text.TextHandler;
import net.sf.sdedit.ui.ImagePaintDevice;

import com.sun.javadoc.Doc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.Tag;
import com.sun.tools.doclets.internal.toolkit.taglets.Taglet;
import com.sun.tools.doclets.internal.toolkit.taglets.TagletOutput;
import com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter;

/**
 * This is a taglet that generates sequence diagrams from the contents of
 * <tt>@sequence.diagram</tt> tags. The diagrams are saved as PNG files in a child
 *                   directory of the javadoc destination directory (as
 *                   specified by the -d option). Inside the html pages
 *                   generated by javadoc, the files are referenced using a
 *                   relative path, so the references remain valid if the pages
 *                   are moved - for example, uploaded to a webserver.
 *                   <p>
 *                   If the first line of the tag content is "quoted" it will be
 *                   used as title for the diagram instead of the default
 *                   "Sequence Diagram:"
 *                   <p>
 *                   <tt>@sequence.diagram</tt> are no inline tags and they can be used inside
 *                   classes, and inside package documentation.
 * 
 * @sequence.diagram "Processing of <tt>@sequence.diagram</tt> tags" user:Actor javadoc:Javadoc[a] sourceFile:File
 *                   /docFile:File taglet:SequenceTaglet
 * 
 * user:javadoc.generateDocumentation() [c:loop for all source files]
 * javadoc:sourceFile.read() javadoc:docFile.new() [c:loop "sequence.diagram"
 * tag found] javadoc:output=taglet.getTagletOutput(tag)
 * javadoc:docFile.append(output) [/c] [/c]
 * 
 * @author Markus Strauch
 * @author ystein Lunde
 */
public class SequenceTaglet implements Taglet
{

    private static final String tagName = "sequence.diagram";

    /**
     * The name of the child directory of the javadoc destination directory
     * where the png files are stored.
     */
    private static final String imageSubDirectory = "sequence-diagrams";

    /**
     * The directory where the png files are stored.
     */
    private File diagramDirectory;

    /**
     * Counts how often an image base name has been used
     */
    private Map<String, Integer> nameCounter;

    /**
     * Registers an instance of this taglet class.
     * 
     * @param tagletMap
     *            used for registering (maps tag names onto taglets)
     */
    public static void register(Map<String, Taglet> tagletMap) {
        SequenceTaglet tag = new SequenceTaglet();
        Taglet t = (Taglet) tagletMap.get(tag.getName());
        if (t != null) {
            tagletMap.remove(tag.getName());
        }
        tagletMap.put(tag.getName(), tag);
    }

    private SequenceTaglet() {
        nameCounter = new HashMap<String, Integer>();
    }

    private String getPathToJavadocDestination(Tag tag)
            throws SequenceTagletException {
        PackageDoc pdoc = null;
        if (tag.holder() instanceof PackageDoc) {
            pdoc = (PackageDoc) tag.holder();
        } else {
            ProgramElementDoc pedoc = (ProgramElementDoc) tag.holder();
            pdoc = pedoc.containingPackage();
        }
        if (pdoc.allClasses() == null || pdoc.allClasses().length == 0) {
            throw new SequenceTagletException(
                    "Cowardly refusing to generate a sequence diagram for an empty package",
                    null);
        }

        String qualifiedName = pdoc.allClasses()[0].qualifiedName();
        String path = "";
        for (int i = 0; i < qualifiedName.length(); i++) {
            if (qualifiedName.charAt(i) == '.') {
                path += "../";
            }
        }
        return path;
    }

    /**
     * Returns the base name of the image to be generated, which is the name of
     * the holder of the tag containing the diagram specification, followed by a
     * unique number.
     * 
     * @param tag
     *            a sequence.diagram tag
     * @return the name of its holder, followed by a unique number
     */
    private String getImageName(Tag tag) {
        String name = tag.holder().name();
        Integer count = nameCounter.get(name);
        if (count == null) {
            count = 1;
        } else {
            count++;
        }
        nameCounter.put(name, count);
        return name + "-" + count;
    }

    /**
     * Creates a sequence diagram image from a part of the contents of a
     * sequence.diagram tag, saves it in the image directory and returns HTML
     * code that references the image.
     * 
     * @param path
     *            the path to the directory where the diagram image is to be
     *            stored (usually a path going upwards, i. e. containing ../'s)
     * @param imageBaseName
     *            the base name of the image to be stored
     * @param source
     *            string array containing the lines of the diagram specification
     * @return the output that is to appear on the javadoc page (i. e. the
     *         &lt;img&gt; tag referencing the image
     * @throws SequenceTagletException
     *             if the creation of the diagram fails
     */
    private String generateOutput(String path, String imageBaseName,
            String[] source) throws SequenceTagletException {
        if (source == null || source.length == 0) {
            return "";
        }
        // Set custom diagram title if first line is quoted
        String diagramTitle = null;
        if (source[0].trim().matches("^[\"'].*[\"']$")) {
            // Set the title and remove the title quote
            diagramTitle = source[0].replaceAll("[\"']", "");
            // Remove the custom title from the sd spec
            source[0] = "";
        }

        StringBuffer buffer = new StringBuffer();

        for (String string : source) {
            string = string.trim();
            if (string.startsWith("<") && string.endsWith(">")) {
                continue;
            }
            buffer.append(string + "\n");
        }
        String specification = buffer.toString().trim();
        if (specification.length() == 0) {
            return "";
        }
        Configuration conf = ConfigurationManager.createNewDefaultConfiguration().getDataObject();
        conf.setHeadWidth(25);
        conf.setMainLifelineWidth(5);
        conf.setSubLifelineWidth(5);
        conf.setThreaded(true);
        conf.setGlue(3);
        ImagePaintDevice device = new ImagePaintDevice();
        TextHandler handler = new TextHandler(specification);
        try {
            new Diagram(conf, handler, device).generate();
        } catch (Exception e) {
            int error = handler.getLineNumber();
            StringBuffer code = new StringBuffer("<br><tt>");
            for (int i = 0; i < source.length; i++) {
                String html = source[i].replaceAll("&", "&amp;").replaceAll(
                        "<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"",
                        "&quot;");
                if (i == error) {
                    html = "<FONT COLOR=\"red\"><U><B>" + html
                            + "</B></U></FONT>";
                }
                code.append(html + "<br>");
            }
            throw new SequenceTagletException(
                    "Malformed diagram specification: " + e.getMessage(),
                    "<DT><HR><B>Sequence Diagram:</B></DT>"
                            + "<DD><B>Could not create sequence diagram: "
                            + "<font color=\"red\">" + e.getMessage()
                            + "</font></B>" + code.toString() + "</DD>");
        }
        String fileName = imageBaseName + ".png";
        File imageFile = new File(diagramDirectory, fileName);

        try {
            device.saveImage(imageFile.getAbsolutePath());
        } catch (IOException ioe) {
            throw new SequenceTagletException("Could not save diagram image: "
                    + ioe.getMessage(), "");
        }
        if (diagramTitle == null) {
            diagramTitle = "Sequence Diagram " + imageBaseName;
        }
        String imageUrl = path + imageSubDirectory + "/" + fileName;
        String anchor = "<A name=\"" + imageBaseName + "\"/>";
        return "<DT><HR><B>" + anchor + diagramTitle + ":</B><P></DT>"
                + "<DD><img src='" + imageUrl + "'></DD>";

    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#getName()
     */
    public String getName() {
        return tagName;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inConstructor()
     */
    public boolean inConstructor() {
        return true;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inField()
     */
    public boolean inField() {
        return true;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inMethod()
     */
    public boolean inMethod() {
        return true;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inOverview()
     */
    public boolean inOverview() {
        return false;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inPackage()
     */
    public boolean inPackage() {
        return true;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inType()
     */
    public boolean inType() {
        return true;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#isInlineTag()
     */
    public boolean isInlineTag() {
        return false;
    }

    private String toString(Tag tag, TagletWriter writer) {
        String output;
        try {
            String path = getPathToJavadocDestination(tag);
            
            output = generateOutput(path, getImageName(tag), tag.text().split(
                    "\n"));
        } catch (SequenceTagletException ste) {
            writer.configuration().message.warning("doclet.in", ste
                    .getMessage(), tag.holder().name());
            return ste.output;
        }
        return output;
    }

    private String toString(Tag[] tags, TagletWriter writer) {
        if (tags != null && tags.length > 0) {
            String output = "";
            for (Tag tag : tags) {
                try {
                    output += generateOutput(getPathToJavadocDestination(tag),
                            getImageName(tag), tag.text().split("\n"));
                } catch (SequenceTagletException ste) {
                    writer.configuration().message.warning("doclet.in", ste
                            .getMessage(), tags[0].holder().name());
                    output += ste.output;
                }
            }
            return output;
        } else {
            return null;
        }
    }

    private void setDestinationDirectory(String dir) {
        File baseDir = new File(dir);
        diagramDirectory = new File(baseDir, imageSubDirectory);
        diagramDirectory.mkdirs();
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#getTagletOutput(com.sun.javadoc.Tag,
     *      com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter)
     */
    public TagletOutput getTagletOutput(Tag tag, TagletWriter writer)
            throws IllegalArgumentException {
        if (diagramDirectory == null) {
            setDestinationDirectory(writer.configuration().destDirName);
        }
        TagletOutput out = writer.getTagletOutputInstance();
        out.setOutput(toString(tag, writer));
        return out;
    }

    /**
     * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#getTagletOutput(com.sun.javadoc.Doc,
     *      com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter)
     */
    public TagletOutput getTagletOutput(Doc holder, TagletWriter writer)
            throws IllegalArgumentException {
        if (diagramDirectory == null) {
            setDestinationDirectory(writer.configuration().destDirName);
        }
        TagletOutput out = writer.getTagletOutputInstance();
        Tag[] tags = holder.tags(getName());
        if (tags.length == 0) {
            return null;
        }
        out.setOutput(toString(tags, writer));
        return out;
    }

    private static class SequenceTagletException extends Exception
    {

        /**
         * Appears on the HTML page
         */
        String output;

        /**
         * 
         * @param warning
         *            the warning that is sent to the doclet
         * @param output
         *            the output that appears on the HTML page
         */
        SequenceTagletException(String warning, String output) {
            super(warning);
            this.output = output;
        }
    }
}
